<?php
/*********************************************
 *  CPG Dragonfly™ CMS
 *********************************************
	Copyright © since 2010 by CPG-Nuke Dev Team
	http://dragonflycms.org

	Dragonfly is released under the terms and conditions
	of the GNU GPL version 2 or any later version

	Modified to support old CPGTPL system
*/

namespace Poodle;

class TPL extends \Poodle\TPL\Context
{
	const
		OPT_PUSH_DOCTYPE = 1,
		OPT_END_PARSER   = 2;

	public static
		$ERROR_MODE = 0; # 0 = html, 1 = parsed html

	public
		$L10N,
		$DTD,

		$bodylayout = '';

	protected
		$tpl_path = 'tpl/default/',
		$tpl_type = 'html';

	private
		$_xml_parser,
		$_total_time = 0;

	function __construct()
	{
		parent::__construct();
		$this->L10N = new \Poodle\L10N();
//		stream_wrapper_register('tpl', 'Poodle\\TPL\\Stream\\Wrapper');
	}

	function __get($key)
	{
		$SQL = \Poodle::getKernel()->SQL;
		switch ($key)
		{
		// Start old CPGTPL stuff
		case 'theme':        return $this->tpl_name;
		case 'THEME_PATH':   return DF_STATIC_DOMAIN.'themes/'.$this->tpl_name.'/';
		case 'REQUEST_URI':  return str_replace('&', '&amp;', substr(URL::uri(),strlen(\Poodle::getKernel()->CFG->server->path)));
		// End old CPGTPL stuff

		case 'bugs':         return \Poodle\Debugger::displayPHPErrors() ? \Poodle\Debugger::report() : null;
		case 'bugs_json':    return \Poodle\Debugger::displayPHPErrors() ? json_encode(\Poodle\Debugger::report()) : null;
		case 'memory_usage': return (is_object($this->L10N) ? $this->L10N->filesizeToHuman(memory_get_peak_usage()) : memory_get_peak_usage());
		case 'parse_time':   return microtime(true)-$_SERVER['REQUEST_TIME_FLOAT'];
		case 'tpl_time':     return $this->_total_time;
		case 'queries':      return is_object($SQL) ? $SQL->total_queries : null;
		case 'queries_time': return is_object($SQL) ? round($SQL->total_time, 4) : null;
		case 'debug_json':
			$r = array();
			if (\Poodle\Debugger::displayPHPErrors()) {
				$bugs = array();
				foreach (\Poodle\Debugger::report() as $file => $log)
					$bugs[] = array('file' => $file, 'log' => $log);
				if ($bugs) $r['php'] = $bugs;
			}
			if (\Poodle::$DEBUG) {
				if (\Poodle::$DEBUG & \Poodle::DBG_MEMORY && function_exists('memory_get_peak_usage')) {
					$r['memory'] = memory_get_peak_usage();
				}
				if (\Poodle::$DEBUG & \Poodle::DBG_PARSE_TIME) {
					$r['parse_time'] = microtime(true)-$_SERVER['REQUEST_TIME_FLOAT'];
				}
				if (\Poodle::$DEBUG & \Poodle::DBG_TPL_TIME) {
					$r['tpl_time'] = $this->_total_time;
				}
				if ((\Poodle::$DEBUG & \Poodle::DBG_SQL || \Poodle::$DEBUG & \Poodle::DBG_SQL_QUERIES) && is_object($SQL)) {
					$r['sql'] = array('count'=>$SQL->total_queries, 'time'=>$SQL->total_time);
					if (\Poodle::$DEBUG & \Poodle::DBG_SQL_QUERIES && $SQL->querylist) {
						$r['sql']['queries'] = array();
						foreach ($SQL->querylist as $f => $q) {
							$r['sql']['queries'][$f] = $q;
						}
					}
				}
				if (\Poodle::$DEBUG & \Poodle::DBG_INCLUDED_FILES) {
					$r['included_files'] = \Poodle::shortFilePath(get_included_files());
					sort($r['included_files']);
				}
				if (\Poodle::$DEBUG & \Poodle::DBG_DECLARED_CLASSES) {
					$r['declared_classes'] = get_declared_classes();
					$r['declared_classes'] = array_slice($r['declared_classes'], array_search('Poodle', $r['declared_classes']));
				}
				if (\Poodle::$DEBUG & \Poodle::DBG_DECLARED_INTERFACES) {
					$r['declared_interfaces'] = get_declared_interfaces();
				}
				// get_defined_functions()
			}
			return str_replace('\\/','/',json_encode($r));
		}
		return parent::__get($key);
	}


	public function init() {}

	# Gecko supports background-image
	public function uaSupportsSelectOptionBgImage()
	{
		return 'gecko' == \Poodle\HTTP\Client::engine()->name;
	}

	public function toString($filename, $data=null, $mtime=0, $options=self::OPT_END_PARSER)
	{
		ob_start();
		if (self::display($filename, $data, $mtime, $options)) {
			return ob_get_clean();
		} else {
			echo ob_get_clean();
			return false;
		}
	}

	public function evalCache($key)
	{
		$ctx = $this;
//		include('cache://'.$key); // eval() workaround
		return false !== eval('?>'.\Poodle::getKernel()->CACHE->get($key));
	}

	public function evalData(&$data)
	{
		$ctx = $this;
//		include('data:text/plain;base64,'.base64_encode($data)); // eval() workaround
		return false !== eval('?>'.$data);
	}

	/**
	 * Benchmark
	 *     Parser:  0.0038 (Tales: 0.001)
	 *     Eval:    0.0033
	 *     include: 0.0034
	 */
	public function display($filename, $data=null, $mtime=0, $options=0)
	{
		if (!$data && empty($filename) && !is_string($filename)) {
			trigger_error('No data to display');
			return false;
		}
		if (!is_string($data)) { $data = false; }
		if ($data && !preg_match('#((tal|i18n|xsl):|(<[^>]+href|src|action|formaction)="/)#',$data)) {
			echo $data;
			return true;
		}

		// Start old CPGTPL stuff
		if (isset($this->cpgtpl_filenames[$filename])) { $filename = $this->cpgtpl_filenames[$filename]; }
		// End old CPGTPL stuff

		$time = microtime(true);
		$this->tpl_file = $filename.'.xml';

		if (!$this->_xml_parser) {
			$this->_xml_parser = new \Poodle\TPL\Parser();
			if ($this->DTD) { $this->_xml_parser->load_dtd($this->DTD); }
		}

		$error = null;
		$parsed = false;
		$CACHE = \Poodle::getKernel()->CACHE;
		$cache_key = null;
		if ($filename) {
			if ($data) {
				$cache_key = "tpl/_db/{$this->tpl_type}/".$this->tpl_file;
			} else {
				if (!$mtime && (($file = \Poodle::getFile("{$this->tpl_path}template/{$filename}.html")) || ($file = $this->findFile($filename)))) {
					$mtime = filemtime($file);
				}
				$cache_key = $this->tpl_path . $this->tpl_type . '/' . $this->tpl_file;
			}
			if ($CACHE->exists($cache_key) && (!$mtime || $CACHE->mtime($cache_key)>$mtime)) {
				if ($options & self::OPT_PUSH_DOCTYPE) $this->push_doctype();
				$this->evalCache($cache_key);
				$parsed = true;
			}
		}
		if (!$parsed) {
			if ($data)
			{
				$parsed = $this->_xml_parser->parse_string($data, $options & self::OPT_END_PARSER);
			}
			// Start old CPGTPL stuff
			else if ($file = \Poodle::getFile("{$this->tpl_path}template/{$filename}.html"))
			{
				$this->_xml_parser->data = \Poodle\TPL\CPGTPL::parse($file);
				$parsed = true;
				$filename = $file;
			}
			// End old CPGTPL stuff
			else if ($filename = $this->findFile($filename))
			{
				$parsed = $this->_xml_parser->parse_file($filename, $options & self::OPT_END_PARSER);
			}
			$data = $this->_xml_parser->data;
			$this->_xml_parser->data = '';
			if ($parsed) {
				if ($options & self::OPT_PUSH_DOCTYPE) $this->push_doctype();
				if (strlen($data)) {
					$level = error_reporting(error_reporting() & ~E_PARSE & ~E_USER_WARNING);
					if (!$this->evalData($data)) {
						$error = error_get_last();
						$error['file'] = $filename;
					} else {
						if ($cache_key) {
							$CACHE->set($cache_key, $data);
						}
						$error = null;
					}
					error_reporting($level);
				} else {
					$error['file'] = $filename;
					$error = array(
						'type' => E_USER_WARNING,
						'message' => 'Parsed data resulted in an empty string',
						'file' => $filename,
						'line' => 0,
					);
				}
			} else if ($this->_xml_parser->errors) {
				$error = $this->_xml_parser->errors[0];
			}
			if ($error) {
				$line  = (int)$error['line'];
				if ((isset($error['type']) && 4 == $error['type']) || (1 === self::$ERROR_MODE && !isset($error['node']))) {
					$lines = preg_split("#<br[^>]*>#", highlight_string($data, true));
				} else {
					$count = 1;
					$xml = $tmp = preg_replace('#\r\n|\r#', "\n", $data?$data:file_get_contents($error['file']));
					while ($count) { $tmp = preg_replace('#(<[a-z][^>\n]*)\n#','$1',$tmp,-1,$count); $line += $count; }
					$lines = preg_split("#<br[^>]*>#", highlight_string($xml, true));
				}
				$lines[$line-1] = '<b style="background-color:#fcc">'.$lines[$line-1].'</b>';
				echo "<h1>{$error['message']} in {$filename} on line {$line}</h1>";
				if (!empty($error['node'])) {
					$node = $error['node'];
					echo "<h2>Tag '{$node['name']}' started in {$node['file']} on line {$node['line']}</h2>";
				}
				echo "\n".implode("<br/>\n", $lines);
				throw new \Exception("{$error['message']} in {$filename} on line {$line}");
				return false;
			}
		}
		$this->_total_time += microtime(true)-$time;
		return true;
	}

	# Check for a valid file
	public function findFile($filename)
	{
		$files = array("{$this->tpl_path}{$this->tpl_type}/{$filename}.xml");
		if ('default' !== basename($this->tpl_path)) {
			$files[] = dirname($this->tpl_path)."/default/{$this->tpl_type}/{$filename}.xml";
		}
		if (preg_match('#(dragonfly|poodle)/([^/]+)/(.+)#',$filename,$i)) {
			$files[] = "includes/{$i[1]}/{$i[2]}/tpl/{$this->tpl_type}/{$i[3]}.xml";
		}
		else if (strpos($filename,'/')) {
			$i = explode('/',$filename,2);
			$files[] = "modules/{$i[0]}/tpl/{$this->tpl_type}/{$i[1]}.xml";
		}
		foreach ($files as $file) {
			if ($file = \Poodle::getFile($file)) return $file;
		}
		trigger_error("\Poodle\TPL::findFile({$this->tpl_path}{$this->tpl_type}/{$filename}.xml): failed to open stream: No such file or directory", E_USER_WARNING);
	}

	// Start old CPGTPL stuff
	protected $cpgtpl_filenames = array();
	public function assign_block_vars($k, array $data) {
		if (strpos($k, '.')) {
			// Nested block.
			$blocks = explode('.', $k);
			$k = $blocks[0];
			if (!isset($this->$k)) { return false; }
			$top = $this->$k;
			$arr = &$top[count($this->$k)-1];
			$blockcount = count($blocks)-1;
			for ($i = 1; $i < $blockcount; ++$i)  {
				if (!isset($arr[$blocks[$i]])) { return false; }
				$arr = &$arr[$blocks[$i]];
				$arr = &$arr[count($arr) - 1];
			}
			// Now we add the block that we're actually assigning to.
			// We're adding a new iteration to this block with the given
			// variable assignments.
			$arr[$blocks[$blockcount]][] = $data;
			$this->$k = $top;
		} else {
			// Top-level block.
			if (!isset($this->$k)) { $this->$k = array(); }
			array_push($this->$k, $data);
		}
		return true;
	}
	public function assign_var($k, $v) { $this->$k = $v; }
	public function assign_vars(array $data) { foreach ($data as $k=>$v) { $this->$k = $v; } }
	public function assign_var_from_handle($k, $handle) { $this->$k = $this->toString($handle); }
	public function set_filenames($filename_array)
	{
		if (!is_array($filename_array)) { return false; }
		foreach ($filename_array as $handle => $filename) { $this->set_handle($handle, $filename); }
		return true;
	}
	public function set_handle($handle, $filename)
	{
		if (empty($handle)) { trigger_error('template error - No handlename specified', E_USER_ERROR); }
		if (empty($filename)) { trigger_error("template error - Empty filename specified for $handle", E_USER_ERROR); }
		$this->cpgtpl_filenames[$handle] = str_replace('.html','',$filename);
	}
	public function destroy() {}
	public function to_string($handle) { return $this->toString($handle); }
	public function unset_block() {}
	public static function getThemes()
	{
		$themes = array();
		$handle = opendir('themes');
		while ($file = readdir($handle)) {
			if (false === strpos($file, '.') && $file != 'CVS' && $file != 'admin' && file_exists("themes/{$file}/theme.php")) {
				$themes[] = $file;
			}
		}
		closedir($handle);
		natcasesort($themes);
		return $themes;
	}
	// End old CPGTPL stuff

	private function push_doctype()
	{
		if ('xml'===$this->tpl_type || $this->_xml_parser->isXML()) {
			echo '<?xml version="1.0" encoding="UTF-8"?>'."\n";
			if (false === stripos($_SERVER['HTTP_ACCEPT'], 'application/xhtml+xml')) {
				// ? 'Content-Type: application/xhtml+xml' : 'Content-Type: text/html' );
				echo '<?xml-stylesheet type="text/xsl" href="'.\Poodle::$URI_BASE.'/inc/msie-xhtml.xsl"?>'."\n";
			}
		}
		echo $this->_xml_parser->doctype();
	}

	protected static function echo_attributes($attribs)
	{
		foreach ($attribs as $name=>$value) {
			if (true===$value) $value = '';
			if (is_string($value) || is_numeric($value)) { echo " {$name}=\"".htmlspecialchars($value,ENT_COMPAT,'UTF-8',false)."\""; }
		}
	}

	protected static function echo_data($data)
	{
		echo htmlspecialchars($data,ENT_NOQUOTES,'UTF-8',false);
	}

	protected static function resolveURI($uri)
	{
		return is_null($uri) ? null : \Poodle\URI::resolve($uri);
	}

	// html5 inputs
	protected static function echo_input_attributes($attribs)
	{
		if (isset($attribs['value']) && isset($attribs['type'])) {
			$fn = 'gmdate';
			$f = null;
			switch ($attribs['type'])
			{
			case 'date':     $f = 'Y-m-d'; break;
			case 'datetime': $f = 'Y-m-d\TH:i:s\Z'; break;
			case 'datetime-local': $f = 'c'; $fn='date'; break;
			case 'month':    $f = 'Y-m'; break;
			case 'time' :    $f = 'H:i:s'; break;
			case 'week':     $f = 'Y-\WW'; break;
			}
			$v = $attribs['value'];
			if ($f && (is_int($v) || ctype_digit($v))) {
				$attribs['value'] = 0==$v ? '' : $fn($f,$v);
			}
			if ($f && $v instanceof \DateTime) {
				$attribs['value'] = $v->format($f);
			}
		}
		if (isset($attribs['autofocus'])) $attribs['autofocus'] = !empty($attribs['autofocus']);
		if (isset($attribs['checked']))   $attribs['checked']   = !empty($attribs['checked']);
		if (isset($attribs['disabled']))  $attribs['disabled']  = !empty($attribs['disabled']);
		if (isset($attribs['multiple']))  $attribs['multiple']  = !empty($attribs['multiple']);
		if (isset($attribs['readonly']))  $attribs['readonly']  = !empty($attribs['readonly']);
		if (isset($attribs['required']))  $attribs['required']  = !empty($attribs['required']);
		if (isset($attribs['selected']))  $attribs['selected']  = !empty($attribs['selected']);
		return static::echo_attributes($attribs);
	}

	protected static function get_valid_option($options)
	{
		foreach ($options as $value) { if ($value) { return $value; } }
		return null;
	}

	/**
	 * helper method for self::path()
	 */
	private static function pathError($base, $path, $current)
	{
		$basename = '';
		$file = '';
		$line = 0;
		# self::path gets data in format ($object, "rest/of/the/path"),
		# so name of the object is not really known and something in its place
		# needs to be figured out
		if ($current !== $path) {
			$pathinfo = " (in path '.../$path')";
			if (preg_match('!([^/]+)/'.preg_quote($current, '!').'(?:/|$)!', $path, $m)) {
				$basename = "'{$m[1]}'";
			}
		} else $pathinfo = '';

		$bt = debug_backtrace();
		foreach ($bt as $i => $item) {
			if ('eval' === $item['function'] || 'include' === $item['function']) {
				$line = $bt[$i-1]['line'];
			}
			if (isset($item['object'])) {
				$file = $item['object']->tpl_file;
				break;
			}
		}

		if (is_array($base)) {
			$msg = "Array {$basename} doesn't have key named '{$current}'{$pathinfo}";
		} else
		if (is_object($base)) {
			$msg = get_class($base)." object {$basename} doesn't have method/property named '{$current}'{$pathinfo}";
		}
		else {
			$msg = trim("Attempt to read property '{$current}'{$pathinfo} from ".gettype($base)." value {$basename}");
		}
		\Poodle\Debugger::error(E_USER_NOTICE, $msg, $file, $line);
	}

	/**
	 * Resolve TALES path starting from the first path element.
	 * The TALES path : object/method1/10/method2
	 * will call : self::path($ctx->object, 'method1/10/method2')
	 *
	 * @param mixed  $base    first element of the path ($ctx)
	 * @param string $path    rest of the path
	 *
	 * @access private
	 * @return mixed
	 */
	private static function path($base, $path)
	{
		if (null === $base) {
			return null;
			self::pathError($base, $path, $path);
		}

		foreach (explode('/', $path) as $current)
		{
			if (is_object($base))
			{
				# look for method. Both method_exists and is_callable are required because of __call() and protected methods
				if (method_exists($base, $current) && is_callable(array($base, $current))) {
					$base = $base->$current();
					continue;
				}

				# look for property
				if (property_exists($base, $current)) {
					$base = $base->$current;
					continue;
				}

				if ($base instanceof \ArrayAccess && $base->offsetExists($current)) {
					$base = $base->offsetGet($current);
					continue;
				}

				if ($base instanceof \Countable && ('length' === $current || 'size' === $current)) {
					$base = count($base);
					continue;
				}

				# look for isset (priority over __get)
				if (method_exists($base, '__isset')) {
					if ($base->__isset($current)) {
						$base = $base->$current;
						continue;
					}
				}
				# ask __get and discard if it returns null
				else if (method_exists($base, '__get')) {
					$tmp = $base->$current;
					if (null !== $tmp) {
						$base = $tmp;
						continue;
					}
				}

				# magic method call
				if (method_exists($base, '__call')) {
					try
					{
						$base = $base->__call($current, array());
						continue;
					}
					catch(\Exception $e){}
				}

			} else

			if (is_array($base)) {
				# key or index
				if (array_key_exists((string)$current, $base)) {
					$base = $base[$current];
					continue;
				}
			} else

			if (is_string($base)) {
				# access char at index
				if (is_numeric($current)) {
					$base = $base[$current];
					continue;
				}
			}

			# if this point is reached, then the part cannot be resolved
			self::pathError($base, $path, $current);
			return null;
		}

		return $base;
	}
}

// Start old CPGTPL stuff
class_alias('Poodle\\TPL', 'cpg_template');
// End old CPGTPL stuff
